<template>
    <div class="ThreeMFViewer" :ref="three.el">
        <div class="container-rb">{{ info }}</div>
    </div>
</template>

<script setup>
    import { ref, reactive, onMounted, onUnmounted, watch, computed } from 'vue';
    import * as THREE from 'three';
    import Stats from 'three/examples/jsm/libs/stats.module.js';
    import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls.js';
    import ThreeMFLoader from './ThreeMFLoader.js';

    // https://stackoverflow.com/questions/29884485/threejs-canvas-size-based-on-container
    function getDomSize(dom) {
        return { width: dom.parentNode.clientWidth, height: dom.clientHeight };
    }

    // 角度转弧度
    function degToRad(degrees) {
        return degrees * (Math.PI / 180);
    }

    // 弧度转角度
    function radToDeg(radians) {
        return radians * (180 / Math.PI);
    }

    function rotationToDegrees(rotation) {
        return {
            x: radToDeg(rotation.x),
            y: radToDeg(rotation.y),
            z: radToDeg(rotation.z)
        };
    }

    const emit = defineEmits([
        'downloadProgress',
        'parseProgress',
        'error',
        'success',
        'modelSelected'
    ]);

    const props = defineProps({
        printerType: { type: String, default: 'Creality K2', required: false }, // 取值 "Creality K2" ｜ "Creality K2 Pro" ｜ "Creality K2 Plus" ｜ "Creality Hi"
        url: { type: String, default: '', required: true }
    });

    let requestId = null;
    let orbitControls = null;

    const loader = new ThreeMFLoader();

    const three = {
        el: ref(),
        scene: null,
        camera: null,
        renderer: null,
        canvasWidth: 0,
        canvasHeight: 0,
        resizeObserver: null, // 监听 canvas 大小变化
        stats: null, // 帧率监测
        modelGroup: new THREE.Group(), // 用于添加模型
        plateGroup: new THREE.Group() // 用于添加打印板
    };

    const state = reactive({
        loading: false,
        model: null // 选中的模型
    });

    const info = computed(() => {
        const { x, y, z } = getPrinterSize(props.printerType);
        return `printerType=${props.printerType}, x=${x}, y=${y}, z=${z}`;
    });

    // 获取打印机的尺寸
    const getPrinterSize = (printer_model) => {
        const printer_model_size = {
            'Creality K2': {
                x: 260,
                y: 260,
                z: 260
            },
            'Creality K2 Pro': {
                x: 300,
                y: 300,
                z: 300
            },
            'Creality K2 Plus': {
                x: 350,
                y: 350,
                z: 350
            },
            'Creality Hi': {
                x: 260,
                y: 260,
                z: 300
            }
        };
        return printer_model_size[printer_model];
    };

    // 添加打印底板
    const addPlate = (printer_model) => {
        const { x: length } = getPrinterSize(printer_model); // 打印底板都是正方形，x=y

        const generatePlate = () => {
            const plate = new THREE.Group();

            {
                // gap=10 的网格
                const color = 0xaaaaaa;
                const divisions = length / 10;
                const gridHelper = new THREE.GridHelper(length, divisions, color, color); // xz 平面
                plate.add(gridHelper);
            }

            {
                // gap=50 的网格
                const color = 0x444444;
                const divisions = length / 50;
                const gridHelper = new THREE.GridHelper(length, divisions, color, color); // xz 平面
                plate.add(gridHelper);
            }

            // todo: 添加其他元素，图片等
            return plate;
        };

        // 添加打印底板
        if (three.plateGroup.children.length === 0) {
            const plate = generatePlate();
            three.plateGroup.add(plate);
        } else {
        }

        // 调整所有 plate 的位置，camera 的参数
        switch (three.plateGroup.children.length) {
            case 1:
                // palte 显示和 cp 中的坐标系保持一致
                const plate = three.plateGroup.children[0];
                plate.rotation.x = -Math.PI / 2;
                plate.position.x = length / 2;
                plate.position.y = length / 2;

                const { x, y, z } = plate.position;
                three.camera.position.set(x, -150, 450);
                three.camera.lookAt(x, y, z);

                // 控制器的旋转中心设置为 plate 的中心
                // console.error(x, y, z);
                orbitControls.target.set(x, y, z);
                orbitControls.update();
                break;
        }
    };

    const refreshPlate = () => {
        console.log('refreshPlate: ' + props.printerType);
        three.plateGroup.clear();
        const count = three.modelGroup.children.length === 0 ? 1 : three.modelGroup.children.length;
        console.log(`设置底板: 个数=${count}, printerType=${props.printerType}`);
        for (let i = 0; i < count; i++) {
            addPlate(props.printerType);
        }
    };

    // setInterval(() => {
    //     const model = three.modelGroup.children[0];
    //     model && (three.modelGroup.rotation.z += 0.1);
    // }, 1000);

    const addAxes = () => {
        const length = 35;
        const origin = new THREE.Vector3(0, 0, 0);
        const infos = [
            // x
            { dir: new THREE.Vector3(1, 0, 0), color: 0xff0000 },
            // y
            { dir: new THREE.Vector3(0, 1, 0), color: 0x00ff00 },
            // z
            { dir: new THREE.Vector3(0, 0, 1), color: 0x0000ff }
        ];
        infos.forEach(({ dir, color }) => {
            const arrowHelper = new THREE.ArrowHelper(dir, origin, length, color);

            // 显示在最上面：解决和其他 line 重合引起 z-fighting
            arrowHelper.cone.material.depthTest = false;
            arrowHelper.line.material.depthTest = false;

            // 增加立体感
            arrowHelper.cone.material = new THREE.MeshStandardMaterial({
                color,
                metalness: 0.6,
                roughness: 0.3
            });

            three.scene.add(arrowHelper);
        });
    };

    onMounted(() => {
        // setup
        actionsThree.setup();
        actionsThree.setupGroup();
        // actionsThree.setupStats();
        actionsThree.setupResizeListener();

        // three.scene.add(new THREE.HemisphereLight(0xffffff, 0x222222, 1.75));
        orbitControls = new OrbitControls(three.camera, three.renderer.domElement);

        actionsThree.animate();

        // 添加坐标轴
        addAxes();

        watch(
            () => props.printerType,
            (newVal, oldVal) => {
                console.log(`-----------------------`);
                console.log(`printerType: ${oldVal} => ${newVal}`);
                if (!newVal) {
                    console.error(`printerType 不能为空`);
                    return;
                }
                refreshPlate();
            },
            { immediate: true }
        );

        watch(
            () => props.url,
            (newVal, oldVal) => {
                console.log(`-----------------------`);
                console.log(`url: ${oldVal} => ${newVal}`);

                if (!newVal) {
                    console.log(`清空 3D 场景`);
                    three.modelGroup.clear();
                    refreshPlate();
                    return;
                }

                console.log(`加载 3mf 文件: ${newVal}`);
                three.modelGroup.clear();
                const url = newVal;
                loader.load(
                    url,
                    (groupModels) => {
                        const plateCount = groupModels.length;
                        console.log('plateCount: ' + plateCount);

                        groupModels.forEach((models) => {
                            models.forEach((model) => {
                                three.modelGroup.add(model);
                            });
                        });
                        state.model = groupModels[0][0];

                        const px = Math.round(state.model.position.x);
                        const py = Math.round(state.model.position.y);

                        const sx = Math.round(state.model.scale.x);
                        const sy = Math.round(state.model.scale.y);
                        const sz = Math.round(state.model.scale.z);

                        const { x, y, z } = rotationToDegrees(state.model.rotation);
                        const rx = Math.round(x);
                        const ry = Math.round(y);
                        const rz = Math.round(z);

                        refreshPlate();

                        emit('modelSelected', { px, py, rx, ry, rz, sx, sy, sz });
                        emit('success');
                    },
                    (xhr) => {
                        const progress = (xhr.loaded / xhr.total) * 100;
                        emit('downloadProgress', progress);
                    },
                    (error) => {
                        emit('error', error);
                    }
                );
            },
            { immediate: true }
        );
    });

    onUnmounted(() => {
        actionsThree.dispose();
    });

    const actionsThree = {
        setup() {
            const { width, height } = getDomSize(three.el.value);

            three.canvasWidth = width;
            three.canvasHeight = height;

            // 透视相机
            three.camera = new THREE.PerspectiveCamera(45, width / height, 1, 10000);
            // three.camera.position.set(0, -600, 900);
            three.camera.lookAt(0, 0, 0);

            three.renderer = new THREE.WebGLRenderer({
                antialias: true,
                alpha: true,
                logarithmicDepthBuffer: true
            });
            three.renderer.setPixelRatio(window.devicePixelRatio); // 显示模糊，要设置
            three.renderer.setSize(width, height);
            three.renderer.setClearColor(0xdddddd, 1);
            // renderer.outputEncoding = THREE.sRGBEncoding;
            three.renderer.autoClear = false;

            three.scene = new THREE.Scene();
            three.scene.background = new THREE.Color(0xdddddd);
            three.scene.add(three.camera);

            three.el.value.appendChild(three.renderer.domElement);

            // light
            let ambit = new THREE.AmbientLight(0xffffff, 2.8);
            three.scene.add(ambit);

            const light = new THREE.DirectionalLight(0xffffff, 0.5);
            light.position.set(0, 1, 0);
            three.scene.add(light);

            const directionalLight = new THREE.DirectionalLight(0xffffff, 0.25);
            directionalLight.position.set(0, 0, 1);
            three.scene.add(directionalLight);
        },
        setupGroup() {
            three.scene.add(three.modelGroup);
            three.scene.add(three.plateGroup);
        },
        setupStats() {
            // 创建性能监视器
            three.stats = new Stats();
            // 设置监视器面板，传入面板id（0: fps, 1: ms, 2: mb）
            three.stats.setMode(0);
            // 设置监视器位置
            three.stats.domElement.style.position = 'absolute';
            three.stats.domElement.style.width = '80px';
            three.stats.domElement.style.height = '48px';
            three.stats.domElement.style.left = '5px';
            three.stats.domElement.style.top = '5px';
            // 将监视器添加到页面中
            three.el.value.appendChild(three.stats.domElement);
        },
        animate() {
            requestId = requestAnimationFrame(actionsThree.animate);
            // three.renderer.clear(true, true, true);
            three.renderer.render(three.scene, three.camera);
            // three.stats?.update();
        },
        resizeWindow() {
            if (!three.el.value) return;
            const { width, height } = getDomSize(three.el.value);
            // console.log('resizeWindow: ', width, height);
            if (width * height !== 0) {
                three.canvasWidth = width;
                three.canvasHeight = height;
                three.renderer.setSize(width, height, true);
                // 正交相机自适应 resize：https://blog.csdn.net/yinge0508/article/details/123114184
                three.camera.aspect = width / height;
                three.camera.updateProjectionMatrix();
                // three.camera.lookAt(0, 0, 0);
            }
        },
        setupResizeListener() {
            three.resizeObserver = new ResizeObserver((entries) => {
                for (const entry of entries) {
                    actionsThree.resizeWindow();
                }
            });
            three.resizeObserver.observe(three.el.value);
            // resize 存在 bug: https://stackoverflow.com/questions/29884485/threejs-canvas-size-based-on-container
            window.addEventListener('resize', actionsThree.resizeWindow, true);
        },
        dispose() {
            requestId && cancelAnimationFrame(requestId);
            three.resizeObserver.disconnect();
            window.removeEventListener('resize', actionsThree.resizeWindow, true);
            // 释放 webgl context
            three.renderer.dispose();
            three.renderer.forceContextLoss();
            three.renderer.context = null;
            three.renderer.domElement = null;
            three.renderer = null;
        }
    };

    const exposeFn = {
        // 修改选中模型的 transform
        changePositionX(value) {
            state.model.position.x = value;
        },
        changePositionY(value) {
            state.model.position.y = value;
        },
        changeScale(x, y, z) {
            state.model.scale.set(x, y, z);
        },
        // value 是角度
        changeRotationX(value) {
            state.model.rotation.x = degToRad(value);
        },
        // value 是角度
        changeRotationY(value) {
            state.model.rotation.y = degToRad(value);
        },
        // value 是角度
        changeRotationZ(value) {
            state.model.rotation.z = degToRad(value);
        }
    };

    defineExpose({ ...exposeFn });
</script>

<style lang="scss" scoped>
    .ThreeMFViewer {
        position: absolute;
        left: 0;
        right: 0;
        top: 0;
        bottom: 0;

        .container-rb {
            position: absolute;
            right: 5px;
            bottom: 5px;
            color: black;
        }
    }
</style>
